package com.googlecode.cswish.struts.interceptor;

import java.util.Collection;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.googlecode.cswish.util.FrameConstant;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.Validateable;
import com.opensymphony.xwork2.ValidationAware;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.interceptor.PrefixMethodInvocationUtil;

public class DefaultWorkflowInterceptor extends MethodFilterInterceptor {

	private static final long serialVersionUID = -3009813207322185078L;

	private static final Log _log = LogFactory.getLog(DefaultWorkflowInterceptor.class);

	private static final String VALIDATE_PREFIX = "validate";
	private static final String ALT_VALIDATE_PREFIX = "validateDo";

	private boolean alwaysInvokeValidate = true;

	private String inputResultName = Action.INPUT;
	
	/**
	 * Determine if {@link Validateable}'s <code>validate()</code> should always
	 * be invoked. Default to "true".
	 * 
	 * @param alwaysInvokeValidate
	 */
	public void setAlwaysInvokeValidate(String alwaysInvokeValidate) {
		this.alwaysInvokeValidate = Boolean.parseBoolean(alwaysInvokeValidate);
	}

	/**
	 * Set the <code>inputResultName</code> (result name to be returned when a
	 * action / field error is found registered). Default to
	 * {@link Action#INPUT}
	 * 
	 * @param inputResultName
	 */
	public void setInputResultName(String inputResultName) {
		this.inputResultName = inputResultName;
	}

	/**
	 * Intercept {@link ActionInvocation} and returns a
	 * <code>inputResultName</code> when action / field errors is found
	 * registered.
	 * 
	 * @return String result name
	 */
	protected String doIntercept(ActionInvocation invocation) throws Exception {
		Object action = invocation.getAction();

		if (action instanceof Validateable) {
			// keep exception that might occured in validateXXX or validateDoXXX
			Exception exception = null;

			Validateable validateable = (Validateable) action;

			try {
				PrefixMethodInvocationUtil.invokePrefixMethod(invocation, new String[] { VALIDATE_PREFIX,
						ALT_VALIDATE_PREFIX });
			} catch (Exception e) {
				// If any exception occurred while doing reflection, we want
				// validate() to be executed
				_log.warn("an exception occured while executing the prefix method", e);
				exception = e;
			}

			if (alwaysInvokeValidate) {
				validateable.validate();
			}

			if (exception != null) {
				// rethrow if something is wrong while doing validateXXX /
				// validateDoXXX
				throw exception;
			}
		}

		boolean returnInput = false;
		if (action instanceof ValidationAware) {
			ValidationAware validationAwareAction = (ValidationAware) action;

			if (validationAwareAction.hasErrors()) {
				if (_log.isDebugEnabled()) {
					_log.debug("Errors on action " + validationAwareAction + ", returning result name 'input'");
				}
				
				returnInput = true;
			}
		} else {
			Object fErrors = invocation.getInvocationContext().getValueStack().findValue("fieldErrors");
			if (size(fErrors) > 0) {
				if (_log.isDebugEnabled()) {
					_log.debug("Errors on action " + fErrors + ", returning result name 'input'");
				}
				returnInput = true;
			}

			Object aErrors = invocation.getInvocationContext().getValueStack().findValue("actionErrors");
			if (size(aErrors) > 0) {
				if (_log.isDebugEnabled()) {
					_log.debug("Errors on action " + aErrors + ", returning result name 'input'");
				}
				returnInput = true;
			}
		}
		
		if (returnInput) {
			// TODO: add special logic for 'search' method?
			return inputResultName;
		}

		// The interface call tell the requested page template
		Object value = invocation.getInvocationContext().getParameters().get(FrameConstant.PARAM_RESULTCODE);
		if (value != null && value instanceof String[]) {
			String[] resultCodes = (String[]) value;
			if (resultCodes.length > 0) {
				final String resultCode = resultCodes[0];
				if (resultCode != null && resultCode.length() > 0 && resultCode.charAt(0) == '$') {
					if (FrameConstant.PARAM_RESULT_EMPTY.equals(resultCode)) {
						invocation.addPreResultListener(new PreResultListener() {
							public void beforeResult(ActionInvocation invocation, String result) {
								invocation.getProxy().setExecuteResult(false);
							}
						});
					} else {
						invocation.addPreResultListener(new PreResultListener() {
							public void beforeResult(ActionInvocation invocation, String result) {
								if (Action.SUCCESS.equals(result)) {
									invocation.setResultCode(resultCode);
								}
							}
						});
					}
				}
			}
		}

		// The service provided the result, execute it directly, some method
		// hasn't the return value, such as 'update'
		final String methodName = invocation.getProxy().getMethod();
		try {
			invocation.addPreResultListener(new PreResultListener() {
				@Override
				public void beforeResult(ActionInvocation invocation, String resultCode) {
					Object ret = invocation.getStack().findValue(methodName);
					if (ret instanceof Result) {
						Result result = (Result) ret;
						invocation.getProxy().setExecuteResult(false);
						try {
							invocation.setResultCode("");
							result.execute(invocation);
						} catch (Exception ex) {
							_log.error(ex);
						}
					}
				}
			});

		} catch (Exception ex) {
			_log.debug("method  " + methodName + " doesn't include the return result");
		}

		String result = invocation.invoke();
		return result;
	}

	private final int size(Object obj) {
		int size = 0;
		if (obj != null) {
			if (obj instanceof Map) {
				Map map = (Map) obj;
				size = map.size();
			} else if (obj instanceof Collection) {
				Collection coll = (Collection) obj;
				size = coll.size();
			}
		}
		return size;
	}
}